const crypto = require('crypto');
const libsignal = require('libsignal');
const ByteBuffer = require('bytebuffer');

function getZeroes(n) {
    const result = new Uint8Array(n);

    const value = 0;
    const startIndex = 0;
    const endExclusive = n;
    result.fill(value, startIndex, endExclusive);

    return Buffer.from(result);
}

function getRandomBytes(n) {
    return crypto.pseudoRandomBytes(n);
}

function hexFromBytes(buffer) {
    return ByteBuffer.wrap(buffer).toString('hex');
}

function base64FromBytes(buffer) {
    return ByteBuffer.wrap(buffer).toString('base64');
}

function bytesFromString(string) {
    return ByteBuffer.wrap(string, 'utf8').toBuffer();
}

function calculateDigest(data) {
    const hash = crypto.createHash('sha256');
    hash.update(data);
    return hash.digest();
}

async function deriveStickerPackKey(packKey) {
    const salt = getZeroes(32);
    const info = bytesFromString('Sticker Pack');

    const [part1, part2] = await libsignal.crypto.deriveSecrets(
        packKey,
        salt,
        info
    );

    return concatenateBytes(part1, part2);
}

function concatenateBytes(...elements) {
    const length = elements.reduce(
        (total, element) => total + element.byteLength,
        0
    );

    let position = 0;
    const result = new Uint8Array(length);

    for (let i = 0, max = elements.length; i < max; i += 1) {
        const element = new Uint8Array(elements[i]);
        result.set(element, position);
        position += element.byteLength;
    }
    if (position !== result.length) {
        throw new Error('problem concatenating!');
    }

    return Buffer.from(result);
}

function encryptAttachment(plaintext, keys, iv) {
    if (
        !(plaintext instanceof ArrayBuffer) &&
        !ArrayBuffer.isView(plaintext)
    ) {
        throw new TypeError(
            `\`plaintext\` must be an \`ArrayBuffer\` or \`ArrayBufferView\`; got: ${typeof plaintext}`
        );
    }

    if (keys.byteLength !== 64) {
        throw new Error('Got invalid length attachment keys');
    }
    if (iv.byteLength !== 16) {
        throw new Error('Got invalid length attachment iv');
    }
    const aesKey = keys.slice(0, 32);
    const macKey = keys.slice(32, 64);

    const ciphertext = libsignal.crypto.encrypt(aesKey, plaintext, iv);
    const ivAndCiphertext = new Uint8Array(16 + ciphertext.byteLength);
    ivAndCiphertext.set(new Uint8Array(iv));
    ivAndCiphertext.set(new Uint8Array(ciphertext), 16);

    const mac = libsignal.crypto.calculateMAC(macKey, Buffer.from(ivAndCiphertext));
    const encryptedBin = new Uint8Array(16 + ciphertext.byteLength + 32);
    encryptedBin.set(ivAndCiphertext);
    encryptedBin.set(new Uint8Array(mac), 16 + ciphertext.byteLength);
    const digest = calculateDigest(Buffer.from(encryptedBin));

    return {ciphertext: Buffer.from(encryptedBin), digest};
}

module.exports = {
    concatenateBytes,
    deriveStickerPackKey,
    encryptAttachment,
    getRandomBytes,
    hexFromBytes,
    base64FromBytes
};
